import escapeRE from 'escape-string-regexp';
import { extname, relative } from 'pathe';
import { encodePath } from 'ufo';

const SegmentParserState = {
  initial: 0,
  static: 1,
  dynamic: 2,
  optional: 3,
  catchall: 4
};

export function resolvePagesRoutes(pages, dir) {
  const routes = [];
  const pageList = Object.entries(pages)
    .map(([file]) => file)
    .sort();
  pageList.forEach((page) => {
    const segments = relative(dir, page)
      .replace(new RegExp(`${escapeRE(extname(page))}$`), '')
      .split('/');
    const route = {
      name: '',
      path: ''
    };
    for (let i = 0; i < segments.length; i++) {
      const segment = segments[i];
      const tokens = parseSegment(segment);
      const segmentName = tokens.map(({ value }) => value).join('');
      route.name += (route.name && '/') + segmentName;
      if (segmentName === 'index' && !route.path) {
        route.path += '/';
      } else if (segmentName !== 'index') {
        route.path += getRoutePath(tokens);
      }
    }
    route.component = pages[`${page}`];
    routes.push(route);
  });
  return prepareRoutes(routes);
}

function prepareRoutes(routes, parent = undefined, names = new Set()) {
  for (const route of routes) {
    if (route.name) {
      route.name = route.name.replace(/\/index$/, '').replace(/\//g, '-');
      if (names.has(route.name)) {
        const existingRoute = findRouteByName(route.name, routes);
        const extra = existingRoute?.name
          ? `is the same as \`${existingRoute.file}\``
          : 'is a duplicate';
        console.warn(
          `[nuxt] Route name generated for \`${route.file}\` ${extra}. You may wish to set a custom name using \`definePageMeta\` within the page file.`
        );
      }
    }
    if (parent && route.path.startsWith('/')) {
      route.path = route.path.slice(1);
    }

    if (route.children?.length) {
      route.children = prepareRoutes(route.children, route, names);
    }

    if (route.children?.find((childRoute) => childRoute.path === '')) {
      delete route.name;
    }

    if (route.name) {
      names.add(route.name);
    }
  }

  return routes;
}

function findRouteByName(name, routes) {
  for (const route of routes) {
    if (route.name === name) {
      return route;
    }
  }
  return findRouteByName(name, routes);
}

export function getRoutePath(tokens) {
  return tokens.reduce((path, token) => {
    return (
      path +
      (token.type === SegmentParserState.optional
        ? `:${token.value}?`
        : token.type === SegmentParserState.dynamic
        ? `:${token.value}()`
        : token.type === SegmentParserState.catchall
        ? `:${token.value}(.*)*`
        : encodePath(token.value))
    );
  }, '/');
}

const PARAM_CHAR_RE = /[\w_.]/;
function parseSegment(segment) {
  let state = SegmentParserState.initial;
  let i = 0;

  let buffer = '';
  const tokens = [];

  function consumeBuffer() {
    if (!buffer) {
      return;
    }
    if (state === SegmentParserState.initial) {
      throw new Error('wrong state');
    }

    tokens.push({
      type: state,
      value: buffer
    });

    buffer = '';
  }

  while (i < segment.length) {
    const c = segment[i];

    switch (state) {
      case SegmentParserState.initial:
        buffer = '';
        if (c === '[') {
          state = SegmentParserState.dynamic;
        } else {
          i--;
          state = SegmentParserState.static;
        }
        break;

      case SegmentParserState.static:
        if (c === '[') {
          consumeBuffer();
          state = SegmentParserState.dynamic;
        } else {
          buffer += c;
        }
        break;

      case SegmentParserState.catchall:
      case SegmentParserState.dynamic:
      case SegmentParserState.optional:
        if (buffer === '...') {
          buffer = '';
          state = SegmentParserState.catchall;
        }
        if (c === '[' && state === SegmentParserState.dynamic) {
          state = SegmentParserState.optional;
        }
        if (
          c === ']' &&
          (state !== SegmentParserState.optional || buffer[buffer.length - 1] === ']')
        ) {
          if (!buffer) {
            throw new Error('Empty param');
          } else {
            consumeBuffer();
          }
          state = SegmentParserState.initial;
        } else if (PARAM_CHAR_RE.test(c)) {
          buffer += c;
        } else {
          // console.debug(`[pages]Ignored character "${c}" while building param "${buffer}" from "segment"`)
        }
        break;
    }
    i++;
  }

  if (state === SegmentParserState.dynamic) {
    throw new Error(`Unfinished param "${buffer}"`);
  }

  consumeBuffer();

  return tokens;
}
